#include <game/input/InputDispatcher.h>
#include <game/controls/Control.h>
#include <game/controls/ControlCollection.h>
#include <game/input/TouchEvent.h>

#include <lib3d/node/Node.h>
#include <lib3d/node/Camera.h>
#include <input/Manager.h>
#include <input/Listener.h>

#include <Object2D.h>
#include <math/Vector2.h>
#include <math/Rect.h>
#include <system/assert.h>
#include <logs/Log.h>
#include <Signals.h>

#include <boost/shared_ptr.hpp>
#include <boost/bind.hpp>

using namespace lib3d;

ARELLO_GAME_NAMESPACE_DECL_START
namespace input {

struct ControlInfo
{
	boost::shared_ptr<controls::Control> Control;
	math::Vector2<float> LocalPosition;
	signals::connection boundsChanged;
	signals::connection controlOrphaned;
};

struct TouchTracker
{
	TouchTracker();
	~TouchTracker();
	void SetTouchId(int value) { this->touchId = value; }

	void TouchStarted(controls::ControlCollection* controls);
	void SetPosition(const math::Vector2<float>& value);
	void TouchReleased();

	void CheckPositions();
private:
	void OnControlOrphaned(controls::Control* c);
	void ObjectMoved(Object2D* obj);

	int touchId;
	std::vector<ControlInfo> underTouch;
	math::Vector2<float> position;

	static const char* LOGGER;
};

const char* TouchTracker::LOGGER = "TouchTracker";

TouchTracker::TouchTracker()
	: position(std::numeric_limits<float>::min(), std::numeric_limits<float>::min())
{ }

TouchTracker::~TouchTracker()
{
	for (std::vector<ControlInfo>::iterator i = this->underTouch.begin(); i != this->underTouch.end(); ++i)
	{
		i->boundsChanged.disconnect();
		i->controlOrphaned.disconnect();
	}
}

struct ControlsPredicateState
{
	std::vector<boost::shared_ptr<controls::Control> > AccumulatedControls;
};

void ControlsPredicate(boost::shared_ptr<controls::Control> c, const math::Vector2<float>& position, ControlsPredicateState* state)
{
	if (c->IsOrphaned())
		return;

	if (!c->GetObject2D())
	{
		LIBA_FAIL_UNEXPECTED();
		return;
	}

	math::Rect<float> b = c->GetObject2D()->GetGlobalBounds();
	if (!b.is_intersects(position))
		return; // Touch is outside this control's bounds 

	state->AccumulatedControls.push_back(c);
}

bool ControlComparer(boost::shared_ptr<controls::Control> l, boost::shared_ptr<controls::Control> r)
{
	if (!l || !r || !l->GetObject2D() || !r->GetObject2D())
	{
		LIBA_FAIL_UNEXPECTED();
		return false;
	}

	return l->GetObject2D()->GetZ() < r->GetObject2D()->GetZ();
}

void TouchTracker::TouchStarted(controls::ControlCollection* controls)
{
	ControlsPredicateState state;
	controls->ForEach(boost::bind(&ControlsPredicate, _1, this->position, &state));
	std::sort(state.AccumulatedControls.begin(), state.AccumulatedControls.end(), &ControlComparer);

	std::vector<boost::shared_ptr<controls::Control> > receivers;
	receivers.reserve(state.AccumulatedControls.size());

	bool exclusiveMode = false;
	for (std::vector<boost::shared_ptr<controls::Control> >::iterator i = state.AccumulatedControls.begin(); i != state.AccumulatedControls.end(); ++i)
	{
		if (exclusiveMode)
		{
			if ((*i)->GetSharingMode() == controls::INPUT_SHARE_EXCLUSIVE)
				receivers.push_back(*i);
		}
		else
		{
			receivers.push_back(*i);
			if ((*i)->GetSharingMode() == controls::INPUT_SHARE_STOP_DIGGING)
				break;
		}
	}

	this->underTouch.clear();
	this->underTouch.reserve(receivers.size());

	for (std::vector<boost::shared_ptr<controls::Control> >::iterator i = receivers.begin(); i != receivers.end(); ++i)
	{
		ControlInfo ci;
		ci.Control = *i;
		ci.LocalPosition = (*i)->GetObject2D()->GlobalToLocal(this->position);
		ci.boundsChanged = (*i)->GetObject2D()->BoundsChanged().connect(
			boost::bind(&TouchTracker::ObjectMoved, this, (*i)->GetObject2D() ));
		ci.controlOrphaned = (*i)->Orphaned().connect(
			boost::bind(&TouchTracker::OnControlOrphaned, this, i->get() ));
		
		this->underTouch.push_back(ci);

		TouchEvent te;
		te.GlobalPoint = this->position;
		te.LocalPoint = ci.LocalPosition;
		te.TouchId = this->touchId;
		(*i)->OnTouchStarted(te);
	}
}

void TouchTracker::SetPosition(const math::Vector2<float>& value)
{
	this->position = value;
	CheckPositions();
}

void TouchTracker::CheckPositions()
{
	const float EPSILON = 0.0001f;

	for (std::vector<ControlInfo>::iterator i = this->underTouch.begin(); i != this->underTouch.end(); ++i)
	{
		math::Vector2<float> localPoint = i->Control->GetObject2D()->GlobalToLocal(this->position);
		if (fabs(localPoint.x - i->LocalPosition.x) >= EPSILON ||
			fabs(localPoint.y - i->LocalPosition.y) >= EPSILON)
		{
			i->LocalPosition = localPoint;
			TouchEvent te;
			te.GlobalPoint = this->position;
			te.LocalPoint = localPoint;
			te.TouchId = this->touchId;
			i->Control->OnTouchMove(te);
		}
	}
}

void TouchTracker::ObjectMoved(Object2D* obj)
{
	const float EPSILON = 0.0001f;
	std::vector<ControlInfo>::iterator i = this->underTouch.end();
	for (std::vector<ControlInfo>::iterator j = this->underTouch.begin(); j != this->underTouch.end(); ++j)
		if (j->Control->GetObject2D() == obj)
		{
			i = j;
			break;
		}

	if (i == this->underTouch.end())
	{
		LIBA_FAIL_UNEXPECTED();
		return;
	}

	math::Vector2<float> localPoint = obj->GlobalToLocal(this->position);
	if (fabs(localPoint.x - i->LocalPosition.x) >= EPSILON ||
		fabs(localPoint.y - i->LocalPosition.y) >= EPSILON)
	{
		i->LocalPosition = localPoint;
		TouchEvent te;
		te.GlobalPoint = this->position;
		te.LocalPoint = localPoint;
		te.TouchId = this->touchId;
		i->Control->OnTouchMove(te);
	}
}

void TouchTracker::TouchReleased()
{
	for (std::vector<ControlInfo>::iterator i = this->underTouch.begin(); i != this->underTouch.end(); ++i)
	{
		TouchEvent te;
		te.GlobalPoint = this->position;
		te.LocalPoint = i->LocalPosition;
		te.TouchId = this->touchId;
		i->Control->OnTouchFinished(te);
		i->boundsChanged.disconnect();
		i->controlOrphaned.disconnect();
	}

	this->underTouch.clear();
}

void TouchTracker::OnControlOrphaned(controls::Control* c)
{
	LOG_DEBUG(LOGGER, "Detected that control at 0x%08X is orphaned", c);
	 
	std::vector<ControlInfo>::iterator i = this->underTouch.end();
	for (std::vector<ControlInfo>::iterator j = this->underTouch.begin(); j != this->underTouch.end(); ++j)
	{
		if (j->Control.get() == c)
		{
			i = j;
			break;
		}
	}

	if (i == this->underTouch.end())
	{
		LOG_DEBUG(LOGGER, "Orphaned control couldn't be found in collection of tracked controls");
		return;
	}

	this->underTouch.erase(i);
}

struct InputDispatcherPrivates
	: public liba::input::Listener
{
	InputDispatcherPrivates()
		: controls(0)
		, invertedCamera(lib3d::Matrix::identity())
		, active(false)
	{
	}

	virtual void on_input_event(const liba::input::Event & event) LIBA_OVERRIDE;

	controls::ControlCollection* controls;
	lib3d::Matrix invertedCamera;
	bool active;
	
	TouchTracker& GetTouchTracker(int touchId);
	void ReleaseTouchTracker(int touchId);
	
	std::map<int, TouchTracker> touchTrackers;
};

TouchTracker& InputDispatcherPrivates::GetTouchTracker(int touchId)
{
	TouchTracker& result = touchTrackers[touchId];
	result.SetTouchId(touchId);
	return result;
}

void InputDispatcherPrivates::ReleaseTouchTracker(int touchId)
{
	std::map<int, TouchTracker>::iterator i = this->touchTrackers.find(touchId);
	if (i != this->touchTrackers.end())
		touchTrackers.erase(i);
}

namespace { 
bool IsValidHardwareName(const std::string& name)
{
	return name == "Mouse" || name == "Mouse2";
}

int GetEventMouseNumber(const std::string& hardwareName)
{
    if (hardwareName == "Mouse")
        return 0;
    return 1;
}
} // anonymous namespace

void InputDispatcherPrivates::on_input_event(const liba::input::Event & event)
{
	if(!active)
		return;

	if (!controls)
		return;
	
	if(!IsValidHardwareName(event.get_hardware_name()))
		return;

	int mouseNumber = GetEventMouseNumber(event.get_hardware_name());

	if (event.get_key_name() == "Left Button")
	{
		if (event.is_pressed())
		{
			this->GetTouchTracker(mouseNumber).TouchStarted(this->controls);
		}
		else if (event.is_released())
		{
			this->GetTouchTracker(mouseNumber).TouchReleased();
		}
	}
	else if (event.get_key_name() == "Axis_X" || event.get_key_name() == "Axis_Y")
	{
		float x = (float)liba::input::Manager::get_axis_position(event.get_hardware_name(), "Axis_X");
		// Y axis is inverted because windows y-coord increases from top to bottom, while engine's one decreases
		float y = (float)-liba::input::Manager::get_axis_position(event.get_hardware_name(), "Axis_Y");

		lib3d::Vector point(x, y, 0);
		lib3d::Vector scenePoint = this->invertedCamera * point;

		GetTouchTracker(mouseNumber).SetPosition(math::Vector2<float>(scenePoint.x, scenePoint.y));
	}
}

InputDispatcher::InputDispatcher()
	: privates(new InputDispatcherPrivates())
{
}

InputDispatcher::~InputDispatcher()
{
}

void InputDispatcher::SetControls(controls::ControlCollection* value)
{
	privates->controls = value;
}

void InputDispatcher::SetCamera(liba::lib3d::node::Camera* value)
{
	if (value)
	{
		lib3d::Matrix invProj = lib3d::Matrix::invert(value->get_desc().projection);
		privates->invertedCamera = value->get_desc().world * invProj;
	}
	else
		privates->invertedCamera = lib3d::Matrix::identity();
}
	
void InputDispatcher::Activate()
{
	privates->active = true;
}

void InputDispatcher::Deactivate()
{
	privates->active = false;
}

} // namespace input
ARELLO_GAME_NAMESPACE_DECL_END